本节代码对应 GitHub 分支: chapter10

仓库传送门 (opens new window)

# 路由相关

首先构建路由。

//routes/index.js
import Search from '../application/Search';

export default [
  {
    path: "/",
    component: Home,
    routes: [
      //...
      {
        path: "/search",
        exact: true,
        key: "search",
        component: Search
      }
    ]
]

现在在 application/Search 目录下新建 index.js:

import React, {useState, useEffect} from 'react';
import { CSSTransition } from 'react-transition-group';
import { Container } from './style';

function Search (props) {
  // 控制动画
  const [show, setShow] = useState (false);
  useEffect (() => {
    setShow (true);
  }, []);
  return (
    <CSSTransition
    in={show}
    timeout={300}
    appear={true}
    classNames="fly"
    unmountOnExit
    onExited={() => props.history.goBack ()}
  >
    <Container>
      <div onClick={() => (setShowfalse)}> 返回 </div>
    </Container>
  </CSSTransition>
  )
}

export default Search;

相应的 style.js 中,我们来完成 Container 组件:

import styled from'styled-components';
import style from '../../assets/global-style';

export const Container = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  z-index: 100;
  overflow: hidden;
  background: #f2f3f4;
  transform-origin: right bottom;
`

当然,为了给 Search 页面进出场的过渡效果,我们加上相关的动画钩子类的编写:

&.fly-enter, &.fly-appear {
  transform: translate3d (100%, 0, 0);
}
&.fly-enter-active, &.fly-appear-active {
  transition: all .3s;
  transform: translate3d (0, 0, 0);
}
&.fly-exit {
  transform: translate3d (0, 0, 0);
}
&.fly-exit-active {
  transition: all .3s;
  transform: translate3d (100%, 0, 0);
}

现在,我们进入 Home 组件,也就是跳转路由的地方,给 Search 组件一个入口。

//application/Home/index.js
<span className="iconfont search" onClick={() => props.history.push ('/search')}>&#xe62b;</span>

现在你点击搜索图标就能进入到 Search 页面,并且进出场都是会带滑动的过渡效果。

好,基础框架搭建就到这里,接下来,我们实现 Search 的具体内容。

# 搜索框基础组件开发

搜索框对于这个模块来说是一个非常关键的子组件,涉及到比较复杂的交互,可以说是这个模块的 "中枢" 部分。

在 baseUI/search-box 目录下,新建 index.js:

import React, {useRef, useState, useEffect, useMemo} from 'react';
import styled from'styled-components';
import style from '../../assets/global-style';
import { debounce } from './../../api/utils';


const SearchBox = (props) => {
  const queryRef = useRef ();
  const [query, setQuery] = useState ('');
  // 从父组件热门搜索中拿到的新关键词
  const { newQuery } = props;
  // 父组件针对搜索关键字发请求相关的处理
  const { handleQuery } = props;
  // 根据关键字是否存在决定清空按钮的显示 / 隐藏
  const displayStyle = query ? {display: 'block'}: {display: 'none'};

  const handleChange = () => {
    // 搜索框内容改变时的逻辑
  };
  const clearQuery = () => {
    // 清空框内容的逻辑
  }

  return (
    <SearchBoxWrapper>
      <i className="iconfont icon-back" onClick={() => props.back ()}>&#xe655;</i>
      <input ref={queryRef} className="box" placeholder="搜索歌曲、歌手、专辑" value={query} onChange={handleChange}/>
      <i className="iconfont icon-delete" onClick={clearQuery} style={displayStyle}>&#xe600;</i>
    </SearchBoxWrapper>
  )
};

下面是 SearchBoxWrapper 的样式部分:

const SearchBoxWrapper = styled.div`
  display: flex;
  align-items: center;
  box-sizing: border-box;
  width: 100%;
  padding: 0 6px;
  padding-right: 20px;
  height: 40px;
  background: ${style ["theme-color"]};
  .icon-back {
    font-size: 24px;
    color: ${style ["font-color-light"]};
  }
  .box {
    flex: 1;
    margin: 0 5px;
    line-height: 18px;
    background: ${style ["theme-color"]};
    color: ${style ["highlight-background-color"]};
    font-size: ${style ["font-size-m"]};
    outline: none;
    border: none;
    border-bottom: 1px solid ${style ["border-color"]};
    &::placeholder {
      color: ${style ["font-color-light"]};
    }
  }
  .icon-delete {
    font-size: 16px;
    color: ${style ["background-color"]};
  }
`

好,现在就让我们来梳理一下搜索框的核心逻辑:

  1. 进场时 input 框应该出现光标
  2. 内容改变时要执行父组件传来的回调
  3. 当父组件点击热门搜索中的关键词时,如果新关键词与现在的 query 不同,则修改 query 并执行回调

现在就让我们来一一实现:

进场出现光标:

useEffect (() => {
  queryRef.current.focus ();
}, []);

query 改变时执行回调:

// 监听 input 框的内容
const handleChange = (e) => {
  setQuery (e.currentTarget.value);
};

// 缓存方法
let handleQueryDebounce = useMemo (() => {
  return debounce (handleQuery, 500);
}, [handleQuery]);

useEffect (() => {
  // 注意防抖
  handleQueryDebounce (query);
}, [query]);

父组件点击了热门搜索的关键字,newQuery 更新:

useEffect (() => {
  if (newQuery !== query){
    setQuery (newQuery);
  }
}, [newQuery]);

还剩下清空的逻辑:

const clearQuery = () => {
  setQuery ('');
  queryRef.current.focus ();
}

目前为止,SearchBox 组件就搭建的差不多了,我们把它对接到 Search 组件中。

//Search/index.js
import SearchBox from './../../baseUI/search-box/index';

// 组件内部
const [query, setQuery] = useState ('');

// 由于是传给子组件的方法,尽量用 useCallback 包裹,以使得在依赖未改变,始终给子组件传递的是相同的引用
const searchBack = useCallback (() => {
  setShow (false);
}, []);

const handleQuery = (q) => {
  setQuery (q);
}
// Container 中删除原来的内容,换成下面的
<Container>
  <div className="search_box_wrapper">
    <SearchBox back={searchBack} newQuery={query} handleQuery={handleQuery}></SearchBox>
  </div>
</Container>

现在打开搜索页面,就能顺利地看到搜索框啦!接下来我们就来开发具体的 Search 组件的逻辑了。

阅读全文